TabView
Scripting provides a modern Tab system aligned with iOS 18+:
TabView — container that manages multiple tabs and switching between them
Tab — a single tab and its associated content
TabSection — a way to group tabs into sections, each with its own configuration and header
Combined with TabView-level options and TabViewCustomization, this enables rich tab layouts, including sidebar representations, customization, and persistence.
This document focuses on:
- How to structure tab content using
TabView, Tab, and TabSection
- How to configure tab bar and sidebar behaviors
- How to use
TabViewCustomization to persist and restore user customizations
1. Basic Usage: TabView + Tab
In the simplest case, TabView hosts multiple Tab elements. Each Tab defines:
- A title and system image for the tab item
- A value used for selection
- An optional role (for example
search)
- The actual view content
1import { TabView, Tab, useObservable } from 'scripting'
2
3function RootView() {
4 const selection = useObservable<number>(0)
5
6 return (
7 <TabView selection={selection}>
8 <Tab
9 title="Home"
10 systemImage="house.fill"
11 value={0}
12 >
13 <HomeView />
14 </Tab>
15
16 <Tab
17 title="Search"
18 systemImage="magnifyingglass"
19 value={1}
20 role="search"
21 >
22 <SearchView />
23 </Tab>
24
25 <Tab
26 title="Settings"
27 systemImage="gearshape.fill"
28 value={2}
29 >
30 <SettingsView />
31 </Tab>
32 </TabView>
33 )
34}
Key points:
TabView selection={selection} binds the current tab to an observable value.
- Each
Tab’s value must match the observable’s type (number or string).
- Tabs with
role="search" integrate with tabViewSearchActivation behavior (see below).
2. Grouping Tabs with TabSection
When you have many tabs, or when you want a sidebar-like structure, use TabSection to group related tabs.
The structure becomes:
1TabView
2 ├─ TabSection
3 │ ├─ Tab
4 │ ├─ Tab
5 │ └─ ...
6 ├─ TabSection
7 │ ├─ Tab
8 │ └─ ...
1function MailRootView() {
2 const selection = useObservable<string>('inbox')
3
4 return (
5 <TabView selection={selection}>
6 <TabSection title="Mailboxes">
7 <Tab
8 title="Inbox"
9 systemImage="tray.full.fill"
10 value="inbox"
11 >
12 <InboxView />
13 </Tab>
14
15 <Tab
16 title="Sent"
17 systemImage="paperplane.fill"
18 value="sent"
19 >
20 <SentView />
21 </Tab>
22 </TabSection>
23
24 <TabSection title="Labels">
25 <Tab
26 title="Important"
27 systemImage="star.fill"
28 value="important"
29 >
30 <ImportantView />
31 </Tab>
32 </TabSection>
33 </TabView>
34 )
35}
If you need a richer header (icon + text + description, etc.), use header instead of title:
1<TabSection
2 header={
3 <HStack spacing={8}>
4 <Image systemName="folder.fill" />
5 <VStack alignment="leading">
6 <Text fontWeight="bold">Projects</Text>
7 <Text fontSize={12} foregroundColor="secondary">
8 Recently opened projects
9 </Text>
10 </VStack>
11 </HStack>
12 }
13>
14 <Tab title="Project A" systemImage="doc.fill" value="projectA">
15 <ProjectAView />
16 </Tab>
17
18 <Tab title="Project B" systemImage="doc.fill" value="projectB">
19 <ProjectBView />
20 </Tab>
21</TabSection>
title and header are mutually exclusive: use one or the other per section.
3. Section-Level Configuration: Layout, Actions, Drag & Drop
TabSection can control how a section is presented and how it behaves.
3.1 tabPlacement
Controls where and how the section’s tabs appear. Common values:
automatic — let the system decide based on environment.
pinned — pins tabs so they remain visible in the bar.
sidebarOnly — show tabs only in the sidebar representation.
Example: a section that only appears in the sidebar:
1<TabSection
2 title="Tags"
3 tabPlacement="sidebarOnly"
4>
5 <Tab title="Important" systemImage="star.fill" value="important">
6 <ImportantView />
7 </Tab>
8</TabSection>
3.2 sectionActions
Provides extra actions associated with a section, such as “Add” or “More”.
1<TabSection
2 title="Lists"
3 sectionActions={
4 <Button
5 title="Add"
6 systemImage="plus"
7 action={addNewList}
8 />
9 }
10>
11 <Tab title="Today" systemImage="sun.max.fill" value="today">
12 <TodayView />
13 </Tab>
14</TabSection>
3.3 Visibility and customization behavior
At the section level you can configure:
- Default visibility in different placements (tab bar, sidebar)
- Customization behavior (whether users can reorder or adjust the section)
Typical use-case: a section that users can reorder in a Tab layout editor:
1<TabSection
2 title="Files"
3 customizationID="files-section"
4 customizationBehavior="reorderable"
5>
6 <Tab title="Recent" systemImage="clock.fill" value="recent">
7 <RecentFilesView />
8 </Tab>
9</TabSection>
3.4 Drag & drop integration
Both TabSection and Tab can participate in drag & drop via:
draggable — logical drag identifier
dropDestination — handler for dropped items
Example:
1<TabSection
2 title="Files"
3 draggable="files-section"
4 dropDestination={items => handleDroppedItems(items)}
5>
6 <Tab title="Recent" systemImage="clock.fill" value="recent">
7 <RecentFilesView />
8 </Tab>
9</TabSection>
4. TabView-Level Configuration
On the TabView (or the view owning the TabView) you can configure global behavior such as:
- Tab bar minimization
- Bottom accessories
- Search activation behavior
- Sidebar header/footer/bottom bar
- Customization state (
tabViewCustomization)
4.1 tabBarMinimizeBehavior (iOS 26.0+)
Controls how the tab bar minimizes in response to scrolling:
automatic
never
onScrollDown
onScrollUp
1<TabView
2 selection={selection}
3 tabBarMinimizeBehavior="onScrollDown"
4>
5 {/* sections + tabs */}
6</TabView>
4.2 tabViewBottomAccessory (iOS 26.0+)
Places a view at the bottom of the TabView—below the tab bar or tab area.
1<TabView
2 selection={selection}
3 tabViewBottomAccessory={
4 <HStack spacing={8}>
5 <Text fontSize={12}>Swipe left or right to switch tabs</Text>
6 <Spacer />
7 <Button title="Got it" action={dismissHint} />
8 </HStack>
9 }
10>
11 {/* sections + tabs */}
12</TabView>
4.3 tabViewSearchActivation (iOS 26.0+)
Configures how search is activated for tabs with role="search":
automatic
searchTabSelection — activate search when the search tab is selected
1<TabView
2 selection={selection}
3 tabViewSearchActivation="searchTabSelection"
4>
5 <Tab title="Home" systemImage="house.fill" value="home">
6 <HomeView />
7 </Tab>
8
9 <Tab
10 title="Search"
11 systemImage="magnifyingglass"
12 value="search"
13 role="search"
14 >
15 <SearchView />
16 </Tab>
17</TabView>
For sidebar-style TabView, you can add:
tabViewSidebarHeader — top area (user info, app logo, etc.)
tabViewSidebarFooter — bottom area (settings, logout)
tabViewSidebarBottomBar — bar between main content and bottom edge
1<TabView
2 selection={selection}
3 tabViewSidebarHeader={
4 <VStack alignment="leading" spacing={4}>
5 <Image systemName="person.circle.fill" fontSize={32} />
6 <Text fontWeight="bold">User Name</Text>
7 <Text fontSize={12} foregroundColor="secondary">
8 Welcome back
9 </Text>
10 </VStack>
11 }
12 tabViewSidebarFooter={
13 <Button title="Settings" systemImage="gearshape" action={openSettings} />
14 }
15 tabViewSidebarBottomBar={
16 <Button title="Upgrade to Pro" systemImage="star.fill" action={upgrade} />
17 }
18>
19 {/* sections + tabs */}
20</TabView>
5. TabViewCustomization: Persisting Layout and Visibility
TabViewCustomization is the core object that represents the customization state of a TabView. It can:
- Track section order
- Track tab order within each section
- Track tab visibility (tab bar vs sidebar)
- Reset section order or visibility
- Be serialized to / from
Data for persistence
The typical pattern is:
- Initialize
TabViewCustomization from storage (if present), otherwise create a new instance.
- Observe changes to it and save serialized data back to storage.
- Use it to query and modify section and tab customizations.
- Pass it into the TabView via
tabViewCustomization.
5.1 Initializing and persisting TabViewCustomization
Below is the correct example using useObservable and Storage:
1const customization = useObservable<TabViewCustomization>(() => {
2 const data = Storage.get('tab_customization')
3 if (data) {
4 return TabViewCustomization.fromData(data) ?? new TabViewCustomization()
5 }
6 return new TabViewCustomization()
7})
8
9useEffect(() => {
10 const listener = (newValue: TabViewCustomization) => {
11 const data = newValue.toData()
12 if (data) {
13 Storage.set('tab_customization', data)
14 }
15 }
16 customization.subscribe(listener)
17 return () => {
18 customization.unsubscribe(listener)
19 }
20}, [])
Explanation:
-
The initializer:
- Reads raw
Data from Storage using the key tab_customization.
- Uses
TabViewCustomization.fromData(data) to recreate a customization object.
- Falls back to
new TabViewCustomization() if the data is invalid or missing.
-
The useEffect:
- Subscribes to changes on the observable.
- Every time the
TabViewCustomization changes, toData() is called and persisted.
- Cleans up the subscription on unmount.
This ensures the layout is restored on launch and any user changes are saved automatically.
5.2 Using TabViewCustomization with TabView
You typically pass the observable itself into the TabView:
1<TabView
2 selection={selection}
3 tabViewCustomization={customization}
4>
5 {/* TabSection + Tab structure */}
6</TabView>
Internally, the Tab system updates the TabViewCustomization object as the user edits the layout, reorders sections, hides tabs, and so on. The observable subscription persists these updates.
5.3 Working with sections: getSection and section order
You can query a section by its customizationID:
1const filesSection = customization.value.getSection('files-section')
A section customization can:
- Expose
tabOrder: the array of tab IDs in this section (or null if not customized).
- Provide
resetTabOrder(): to restore the original system-defined order of tabs in this section.
Example:
1function resetFilesSectionOrder() {
2 const section = customization.value.getSection('files-section')
3 section?.resetTabOrder()
4}
5.4 Working with tabs: getTab and visibility
You can query a tab by its customizationID:
1const importantTab = customization.value.getTab('important-tab')
A tab customization exposes:
tabBarVisibility — read-only current visibility in the tab bar.
sidebarVisibility — read/write visibility in the sidebar representation.
Example: hiding a tab from the sidebar only:
1const importantTab = customization.value.getTab('important-tab')
2if (importantTab) {
3 importantTab.sidebarVisibility = 'hidden'
4}
This allows you to:
- Implement “show/hide in sidebar” toggles.
- Sync visibility with user preferences or other settings.
5.5 Global resets: section order and visibility
Two convenience methods reset parts of the customization:
1customization.value.resetSectionOrder()
2customization.value.resetVisibility()
Typical usage: a “Reset layout” button.
1<Button
2 title="Restore Default Layout"
3 action={() => {
4 customization.value.resetSectionOrder()
5 customization.value.resetVisibility()
6 }}
7/>
This restores both:
- Section ordering
- Tab visibility (in tab bar and sidebar)
to their original default state.
6. Relationship with tabItem-based API
Earlier examples in the project may use a tabItem view modifier to configure tab labels. That approach is documented elsewhere and is suitable for simple Tab views.
However, for:
- Grouped tabs (
TabSection)
- Sidebar representations
- Tab reordering and visibility customization (
TabViewCustomization)
- Per-section actions and layouts
you should use the TabView + Tab + TabSection + TabViewCustomization structure described here.
It provides a clearer model, matches modern iOS Tab APIs, and is designed to work seamlessly with customization and persistence.